ERC721 eXtension
TL;DR;
Loom Networkが提案して実装しているよ!
ERC721で表現されているCollectibles(ゲームのカードなど)をbatch transfer出来るようにしようとしている
ERC721との後方互換性を意識している
同じERC721の中に複数の「クラス(種類)」の概念を導入したい
集めると価値が出るネコとかカードとか
サンプル実装はIDの採番ルールで工夫をして複数の種類のCollectibleを同時に扱えるようにしている
背景や問題意識
ERC721をまとめて取り扱うような仕様は沢山提案されているが、全く新しい仕様として提案されてしまっているので既存ウォレットやマーケットプレイスと互換性がない
however, every single instance sacrifices compatibility with existing wallets and marketplaces by creating an entirely new specification.
1つのコントラクト内に複数のアイテム「クラス」が含まれるようにする
How To & approach
ERC1178とERC721を組み合わせて作る
ERC1178 ... ERC20の拡張(複数「クラス」を扱えるようにしたERC20)
ERC20との類似性/ERC721との互換性を保つことで 読んで理解するのが簡単なコードにする
興味を持った開発者/ユーザは誰でもコードをauditできる
肥大化を避ける
必要な機能を実装するための最小限の仕様を提供する
ゲームのみならず、他のものにも役に立つようにする
https://gyazo.com/79f0f0126735358931adb316f69a0f93
ベースになっているERC1178とは?
概要
(議論はあんまり盛り上がってない)
ERC20に「クラス」の概念を付加したもの
A standard interface for multi-class fungible tokens.
ここで言う「クラス」はオブジェクト指向のアレではなく株式の種別のようなもの
preferred / common / restricted 等
優先株式 / 普通株式 / 劣後株式
既存のERC721の仕様でもうまくやると実装することは可能だがガスコストなどが問題になるのので新しい仕様を提案する
ERC20とは互換性がなさそうに見える
_classIDをtransferの引数にとったりするので)
Aside: In theory, while it is possible to implement tokens with classes using the properties of token structs in ERC-721 tokens, gas costs of implementing this in practice are prohibitive for any non-trivial application.
code: IERC1178.js
// ERC20と同じ。
function name() constant returns (string name)
function className(uint256 classId) constant returns (string name)
// ERC20 と同じ(コントラクトで管理しているトークン総量を返す(全クラス))
function totalSupply() constant returns (uint256 totalSupply)
// ERC20 と同じ(コントラクトで管理している各クラスごとのトークン総量を返す)
function individualSupply(uint256 _classId) constant returns (uint256 individualSupply)
// クラスごと, ownerごとに残高を返す
function balanceOf(address _owner, uint256 _classId) constant returns (uint256 balance)
// _ownerがownerとして登録されているクラスのidを返す
// 権限の弱いトークンはAさん、権限の強いトークンはBさんなど、クラスごとに管理主体を分けることが出来る
function classesOwned(address _owner) constant returns (uint256[] classes)
// Basic Ownership
function approve(address _to, uint256 _classId, uint256 quantity)
function transfer(address _to, uint256 _classId, uint256 quantity)
// Advanced Ownership & Exchnage
// 引数で与えたレートでのトークン交換をapproveする関数
function approveForToken(uint256 classIdHeld, uint256 quantityHeld, uint256 classIdWanted, uint256 quantityWanted)
// 引数で与えたレートでのトークン交換を実行する関数
// Of course, it is possible to create an implementation where calling this function implicitly assumes approval and the transfer is completed in one step.
function exchange(address to, uint256 classIdPosted, uint256 quantityPosted, uint256 classIdWanted, uint256 quantityWanted)
// transferFrom(address from, address to, uint256 classId)
そんなに激しいロジックじゃないからVyperで実装してみようかな...
ERC721xに戻る
使い方としてはERC721を継承した上でExtensionとして追加で継承する
code: ERC721Token.js
// こんなノリ
// Packed NFT that has storage which is batch transfer compatible
contract ERC721XTokenNFT is ERC721, SupportsInterfaceWithLookup {
Backward Compatibilityという意味では良いアプローチに思える
(対応していないウォレットだと個々のCollectibleを通常通りに扱える)
インターフェイス
code: ERC721X.js
contract ERC721X {
function implementsERC721X() public pure returns (bool);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function balanceOf(address owner) public view returns (uint256);
function balanceOf(address owner, uint256 tokenId) public view returns (uint256);
function tokensOwned(address owner) public view returns (uint256[], uint256[]);
function transfer(address to, uint256 tokenId, uint256 quantity) public;
function transferFrom(address from, address to, uint256 tokenId, uint256 quantity) public;
// Fungible Safe Transfer From (ERC721と同じでNFTにも使えるように見えるが...?)
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount) public;
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount, bytes data) public;
// Batch Safe Transfer From
function safeBatchTransferFrom(address _from, address _to, uint256[] tokenIds, uint256[] _amounts, bytes _data) public;
function name() external view returns (string);
function symbol() external view returns (string);
// Required Events
event TransferWithQuantity(address indexed from, address indexed to, uint256 indexed tokenId, uint256 quantity);
event TransferToken(address indexed from, address indexed to, uint256 indexed tokenId, uint256 quantity);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
event BatchTransfer(address indexed from, address indexed to, uint256[] tokenTypes, uint256[] amounts);
}
いろいろな「クラス」を取り扱うために
トークンIDを16コずつのbinに切って管理する
どのトークンIdがどのbinに所属するかを返すメソッドを生やしている
code: ERC721X.js
function _mint(uint256 _tokenId, address _to) internal {
require(!exists(_tokenId), "Error: Tried to mint duplicate token id");
_updateTokenBalance(_to, _tokenId, 1, ObjectLib.Operations.REPLACE); // ここで諸々工夫
allTokens.push(_tokenId);
emit Transfer(address(this), _to, _tokenId);
}
function _updateTokenBalance(
address _from,
uint256 _tokenId,
uint256 _amount,
ObjectLib.Operations op
)
internal
{
// token IDの所属するbinと、それに対応するindexを取得する
(uint256 bin, uint256 index) = _tokenId.getTokenBinIndex();
packedTokenBalance_frombin.updateTokenBalance( index, _amount, op
);
}
code: ERC721X.js
mapping(address => mapping(uint256 => uint256)) packedTokenBalance;
/**
* @dev return the _tokenId type' balance of _address
* @param _address Address to query balance of
* @param _tokenId type to query balance of
* @return Amount of objects of a given type ID
*/
function balanceOf(address _address, uint256 _tokenId) public view returns (uint256) {
// using でgetTokenBinIndexを呼べるようにしている
(uint256 bin, uint256 index) = _tokenId.getTokenBinIndex();
// binごとにバランスを管理
return packedTokenBalance_addressbin.getValueInBin(index); }
code: ObjectLib.js
// Constants regarding bin or chunk sizes for balance packing
uint256 constant TYPES_BITS_SIZE = 16; // Max size of each object
uint256 constant TYPES_PER_UINT256 = 256 / TYPES_BITS_SIZE; // Number of types per uint256
// tokenid = 0 - 15 -> bid = 0, index = 0 - 15
// tokenid = 16 - 31 -> bid = 1, index = 0 - 15
// token id = 256 - 271 -> bid = 16, index = 0 - 15
// tokenidを16コずつのbinで切って管理
function getTokenBinIndex(uint256 _tokenId) internal pure returns (uint256 bin, uint256 index) {
bin = _tokenId * TYPES_BITS_SIZE / 256;
index = _tokenId % TYPES_PER_UINT256;
return (bin, index);
}
// mintの時に呼び出される処理
// function
function updateTokenBalance(
uint256 _binBalances,
uint256 _index,
uint256 _amount,
Operations _operation) internal pure returns (uint256 newBinBalance) {
// ... 省略 NFTのmintのときは{
newBinBalance = writeValueInBin(_binBalances, _index, _amount);
}
// 新規発行時 / 追加発行時 / burn時
function writeValueInBin(uint256 _binValue, uint256 _index, uint256 _amount) internal pure returns (uint256) {
require(_amount < 2**TYPES_BITS_SIZE, "Amount to write in bin is too large");
// Mask to retrieve data for a given binData
uint256 mask = (uint256(1) << TYPES_BITS_SIZE) - 1;
// Shift amount
uint256 leftShift = 256 - TYPES_BITS_SIZE * (_index + 1);
return (_binValue & ~(mask << leftShift) ) | (_amount << leftShift);
}
Conclusion
実装で工夫系ではあるがBackward Compatibleに作ろうとしているのでERC1155より筋が良さそう
Fungibleなassetも同時に扱えるようにしている
ERC721xのFungibleなasset != 通常のERC20
References